/**
 * 
 */
package qy.jalgotrade.stratanalyzer.returns;

import com.google.common.math.DoubleMath;

import qy.jalgotrade.broker.InstrumentTraits;
import qy.jalgotrade.utils.CommonUtils;

/**
 * Helper class to calculate PnL and returns over a single instrument (not the whole portfolio).
 * 
 * @author qy
 *
 */
public class PositionTracker {

	private InstrumentTraits __instrumentTraits;

	private double __pnl;

	/**
	 * Volume weighted average price per share.
	 */
	private double __avgPrice;

	private double __position;

	private double __commissions;

	/**
	 * The total amount commited to this position.
	 */
	private double __totalCommited;

	public PositionTracker(InstrumentTraits instrumentTraits) {

		__instrumentTraits = instrumentTraits;
		__pnl = 0.0;
		__avgPrice = 0.0;
		__position = 0;
		__commissions = 0.0;
		__totalCommited = 0.0;
	}

	public double getPosition() {

		return __position;
	}

	public double getAvgPrice() {

		return __avgPrice;
	}

	public double getCommissions() {

		return __commissions;
	}

	public void reset() {

		__pnl = 0.0;
		__avgPrice = 0.0;
		__position = 0;
		__commissions = 0.0;
		__totalCommited = 0.0;
	}

	/**
	 * 
	 * @return
	 */
	public double getPnL() {

		return getPnL(Double.NaN);
	}

	/**
	 * 
	 * @param price
	 * @return
	 */
	public double getPnL(double price) {

		return getPnL(price, true);
	}

	/**
	 * Return the PnL that would result if closing the position a the given price. Note that this will
	 * be different if commissions are used when the trade is executed.
	 * 
	 * @param price
	 * @param includeCommissions
	 * @return
	 */
	public double getPnL(double price, boolean includeCommissions) {

		double ret = __pnl;
		if (!Double.isNaN(price)) {
			ret += (price - __avgPrice) * __position;
		}
		if (includeCommissions) {
			ret -= __commissions;
		}
		return ret;
	}

	/**
	 * 
	 * @return
	 */
	public double getReturn() {

		return getReturn(Double.NaN);
	}

	/**
	 * 
	 * @param price
	 * @return
	 */
	public double getReturn(double price) {

		return getReturn(price, true);
	}

	/**
	 * 
	 * @param price
	 * @param includeCommissions
	 * @return
	 */
	public double getReturn(double price, boolean includeCommissions) {

		double ret = 0;
		double pnl = getPnL(price, includeCommissions);
		if (__totalCommited != 0.0) {
			ret = pnl / __totalCommited;
		}
		return ret;
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 */
	private void __openNewPosition(double quantity, double price) {

		__avgPrice = price;
		__position = quantity;
		__totalCommited = __avgPrice * Math.abs(__position);
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 */
	private void __extendCurrentPosition(double quantity, double price) {

		double newPosition = __instrumentTraits.roundQuantity(__position + quantity);
		__avgPrice = (__avgPrice * Math.abs(__position) + price * Math.abs(quantity)) / Math.abs(newPosition);
		__position = newPosition;
		__totalCommited = __avgPrice * Math.abs(__position);
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 */
	private void __reduceCurrentPosition(double quantity, double price) {

		// Check that we're closing or reducing partially:
		assert DoubleMath.fuzzyCompare(
		        (double) __instrumentTraits.roundQuantity(Math.abs(__position)) - Math.abs(quantity), 0.0,
		        CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) >= 0;
		double pnl = (price - __avgPrice) * quantity * -1;

		__pnl += pnl;
		__position = __instrumentTraits.roundQuantity(__position + quantity);
		if (DoubleMath.fuzzyEquals(__position, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
			__avgPrice = 0.0;
		}
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 * @param commission
	 */
	public void update(double quantity, double price, double commission) {

		assert !DoubleMath.fuzzyEquals(quantity, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) : "Invalid quantity";
		assert price > 0.0 : "Invalid price";
		assert commission >= 0.0 : "Invalid commission";

		if (DoubleMath.fuzzyEquals(__position, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
			__openNewPosition(quantity, price);
		} else {
			// Are we extending the current position or going in the opposite direction ?
			double currPosDirection = __position / Math.abs(__position);
			double tradeDirection = quantity / Math.abs(quantity);

			if (DoubleMath.fuzzyCompare(currPosDirection * tradeDirection, 0,
			        CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) {
				__extendCurrentPosition(quantity, price);
			} else {
				/*
				 * If we're going in the opposite direction we could be:
				 * 1: Partially reducing the current position.
				 * 2: Completely closing the current position.
				 * 3: Completely closing the current position and opening a new one in the opposite direction.
				 */
				if (Math.abs(quantity) <= Math.abs(__position)) {
					__reduceCurrentPosition(quantity, price);
				} else {
					double newPos = __position + quantity;
					__reduceCurrentPosition(-__position, price);
					__openNewPosition(newPos, price);
				}
			}
		}
		__commissions += commission;
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 */
	public void buy(double quantity, double price) {

		buy(quantity, price, 0.0);
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 * @param commission
	 */
	public void buy(double quantity, double price, double commission) {

		assert quantity > 0 : "Invalid quantity";
		update(quantity, price, commission);
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 */
	public void sell(double quantity, double price) {

		sell(quantity, price, 0.0);
	}

	/**
	 * 
	 * @param quantity
	 * @param price
	 * @param commission
	 */
	public void sell(double quantity, double price, double commission) {

		assert quantity > 0 : "Invalid quantity";
		update(-quantity, price, commission);
	}
}
